Windows主机入侵检测与防御内核技术深入解析
一
第1章内网安全与主机防御
1.1复杂问题的简单起源
一切起源于很多年前的一个下午,办公室里的电脑刚经过大规模的折腾。有人的机器感染了病毒,导致了局域网内部的感染,很多机器无法工作,纷纷被格式化重装。
作为专门开发安全软件的信息安全部竟然被病毒感染,这显然是很打脸的一件事。那时办公室里也安装了杀毒软件,但效果不佳。事后溯源发现是有人收到了小广告邮件,内含带毒的网页链接。网页打开之后浏览器自动安装了病毒。
这时一个留着小山羊胡子的同事灵光一现,说:“这个病毒不管它是怎么装上的,一定要加载一个可执行文件不是?”
“这可不一定。”另一个胖同事反驳。
“网页可以利用你的浏览器漏洞用缓冲溢出之类的方法注入代码执行。这样就不需要生成一个可执行文件了。”
“执行?那执行完了呢?下次它怎么启动自己呢?”
“下次它当然是感染了其他的可执行文件来启动自己啦。”
“那最终不还是得加载一个被感染的可执行文件?”
“这……”
恶意代码并非一定要表现为一个新生成或者被修改的可执行文件。只是那时的安全世界还远没有现在这么复杂。见对方无言以对,山羊胡子一拍手,神采飞扬地说:“我把每个需要用的可执行文件算一个哈希值存起来当白名单。后面系统加载任何可执行模块的时候我都计算一次哈希值和白名单比较。如果不在白名单中的就一律阻止!这不就可以防住所有病毒了?”
“你这个,别人很容易破坏你的吧?”
胖同事显然不相信困扰人类已久的安全问题竟然有如此简单的解决方案。
“比如摘掉你的监控,或破坏你的判断的逻辑。”
“它要破坏我?我做在Windows内核里。它要破坏我那也得先加载一个内核模块是不?”
山羊胡同事的眼里闪着狡黠的光,把这个问题变成了一个先有鸡还是先有蛋的问题。他设想中的系统阻止了一切可疑的可执行文件的加载,那么病毒自然也就没有任何机会去加载一个内核模块。
这个逻辑并不严谨。因为Windows内核以及他开发的驱动也未必没有漏洞。如果病毒能利用这些漏洞来破坏内核的防护是有可能做到的。但这显然使得入侵难度骤然升级。
“但你怎么知道哪些可执行文件是我们要用的呢?”
“这不简单吗?办公机上已经安装的这些就是要用的。别的就是不许用的,我扫描一遍就知道了。”
“这个……好像也可以做做看。”
胖同事似乎被他说服了。
山羊胡子说干就干。他先做了一个简单的扫描工具。这个工具会扫描测试用的虚拟机上所有的可执行文件,对每个文件都计算md5并存入一个白名单列表中。他假定这时候测试机是干净的。
然后他写了一个Windows驱动来监控所有模块的加载。当每个模块他都会找到对应的文件并计算整个文件的md5去和白名单比较。当白名单比较失败的时候,系统会弹出他设计的一个对话框,提示该模块被阻止加载。
然后他在虚拟机中点开了那个导致几乎整个办公室感染的带毒网站。果然,叮地一声,一个携带着ActiveX控件的DLL被拒绝加载的对话框弹了出来!网页正常加载,测试机并未感染病毒。
后来他就一直自己安装着那东西,在办公室里很开心地去网上冲浪了。
1.2内网与主机防御
第1节的起源故事涉及到了办公室网络、网络攻击和在主机上进行的攻击防御。这正是本书所需要讨论、设计和实现的东西。在这我将首先要明确何为内网,以及何为“主机防御”系统。至于网络攻击,我将放在第3节。
本书所谓的“内网”是相对于“外网”而言。一般地说,如果我们构建服务器对外提供服务,那么面向的是外网,需要关注的是外网或者说服务器的安全。另一方面,公司内部所构建的用于办公、开发的环境,则是所谓“内网”,我们需要关注的是内网的安全。
因此本书的“内网”一词正如第1节的故事中提到的,特指某种机构或组织(比如公司)内部用于内部日常活动(比如办公和开发)的网络环境。这些环境有如下几个显著的特点:
(1)很多情况下,内网在物理上是一个局域网。但很多分布在不同地理位置、由广域网连接的不同的分支机构,可能使用技术手段(比如VPN[1]),相互连成一个逻辑上的局域网。但总体而言,内网和外网之间有一定程度的隔离,有相对较强的防护措施。正常情况下从外网黑客不能直接访问到内网的机器。
(2)大多数情况下,内网和外网又不是完全隔离的。出于各种原因,内网需要能够访问外网获得资料,或是进行邮件通信、实时通信等。少数情况下内网会有完全物理隔离的要求。本书会把物理隔离也视为安全防范措施的一种来进行讨论。
(3)内网内部的机器之间往往能够直接访问。机器与机器之间的防护相对薄弱。因此我们会看到很多黑客控制内网中一台机器后,迅速“横向移动”导致整个内网沦陷的例子。
(4)大多数情况下,这些机器用于办公或者软件开发,会安装Windows系统,但不排除部分公司企业或者其他单位有特殊原因而采用其他的操作系统。本书的代码以Windows为主,同时会简要讨论其他如Linux等系统上的主机防御的可能实现办法。
本书讨论的“主机防御”实际上是“基于主机的入侵检测与防御系统”的简称。各种资料中常见的缩写名词HIDS表示的是“基于主机的入侵检测”而HIPS则是“基于主机的入侵防御”。但检测与防御本身密不可分,因此我将统一简称为“主机防御”。
主机防御的本质特点是它是安装在内网用户主机,也就是内网办公所用的每台电脑上的。它依据的是主机的状态和活动。因此我们可以看到第1节中山羊胡子同事所开发的系统,是跑在他自己的机器,也就是用户主机上的。
与主机防御相对的是基于网络的入侵检测与防御系统,简称分别为NIDS(检测)与NIPS(防御)。NIDS/NIPS一般是安装在硬件防火墙、堡垒主机等专用设备上,通过过滤网络数据来起到检测和防御的作用。
在其他的资料中,我们还会看到一个很容易和“主机防御”混淆的词语“主动防御”。
“主动防御”在大多数情况下,是指基于安全系统的一种特性,或代指拥有这种特性的安全系统。它是相对“被动扫描”而言的。被动扫描是杀软最早期的形式,通过特征码扫描恶意代码。主动防御则会根据未知病毒可能的攻击方式才采取主动的措施去进行抵御。
现代主机防御系统基本都具备了“主动防御”的特性。本书中大部分代码实现的功能都属于“主动防御”的范畴。
1.3针对内网的攻击
想象一下在1.1节的故事中,如果对方针对该公司内网的攻击并不是简单地发小广告,而是窃取某个项目的源码,那么常见的模式会是这样几步。
(1)网络侦察。黑客在网络上收集与该项目有关的开发人员的信息。举例而言,至少获得一个该项目开发人员的办公邮箱地址。
(2)初始访问。黑客和目标用户的第一次接触。在本书开头这个例子中的操作是黑客给目标发出一封精心构造的邮件。
(3)执行。目标点击了邮件中的链接,导致了恶意代码的执行。当然真正的执行方式多种多样,并不限于钓鱼邮件的方式。
(4)提权。获取系统根[2]权限。通常国内大部分Windows用户直接使用管理员权限登录,因此这一步甚至可以直接免除。少数管理严格的场景下,被执行的恶意代码只有普通用户的权限,需要提权才能实现后面的步骤。
(5)持久驻留。恶意代码执行之后,如果关机重启就没有了显然不是攻击者想要的。有很多方法让恶意代码留在系统中每次都随系统启动。一般而言,现实现提权再实现持久驻留会更方便,因此排在提权之后。
(6)潜藏。在实现了持久驻留之后,如果很容易被杀软或者防御软件发现那也是没有意义的。因此必须实现潜藏。通常而言,提权对潜藏有极大的帮助。因此潜藏排在提权和持久驻留之后。
(7)命令与控制。这是在被感染的机器上实现一个与攻击者通信的机制,使得攻击者随时可以对被感染机器发出命令来实现控制。这是为实现后面更多步骤的准备。
(8)内网侦察。内网侦察和网络侦察不同。网络侦察是在外部公开的网络上搜集信息。内网侦察则是已实现命令与控制的情况下,控制内网某台被感染的机器对整个内网展开的侦察,目的是为了了解内网的安全设施、网络拓补、有价值的目标的存储情况。这也是为下面步骤做的热身运动。
(9)横向移动。当内网已经被侦察清楚,攻击者就会开始横向移动,即把恶意代码从一台机器感染到另一台机器。要么尽可能地控制更多机器,要么精准控制需要窃取的信息所在的机器。
(10)渗出。渗出即是黑客窃取目标项目代码的过程。他已经可以用命令来控制存储项目代码的机器,那么他当然需要将这些代码发出内网。这个过程即是渗出。
(11)影响。和渗出稍有不同,影响的目标并非是窃取信息,而是对目标进行修改导致目标发生黑客所期望的异常。
比如有的影响是简单地让系统尽可能被破坏,阻滞该项目的进程;也有的影响仅仅是修改某个特定的数据,使得某方得到收益而造成另一方的损失。常见的勒索病毒也是影响的方式之一。
以上模式参考了MITRE ATT&CK模型[3]ATT&CK矩阵,我做了少量删改。网络攻击行为并不需要全部具备以上所有的步骤,但至少具备其中之一。具有其中(5)(6)(7)的威胁就是常见的APT(高级可持续威胁)。
1.4纵深防御的思想
现代的安全观念是针对所有的威胁步骤进行纵深防御。也就是节节防守。这是因为和软件不可能消灭所有漏洞一样,安全系统也是不可能没有漏洞。指望某一个技术在某个一个节点上掐灭所有威胁那是不可能的。
我们只能尽可能地在每个节点上进行防御,并指望即便某个节点被突破了,其他的节点还能防御住攻击。或者即便攻击没有被防住,但是我们至少能在某些节点及时发现攻击的迹象而及时采取所示防止损失扩大化。
本书也秉持这样的思想。我们在攻击威胁的每一步尽可能地进行防御和检测。因此考虑到1.3节中的种种攻击手段,我们应该进行如下的步步防御:
(1)针对侦察,我们必须尽量隐匿内部的信息,确保只有必要的信息才公开在网络上。
(2)针对初始访问,我们必须考虑各种可能访问的入口。如来自外网的邮件、内部人员访问的外部网络、下载的外网内容、通过实时通信软件收到的外部文档或其他请求(如远程控制)等。
(3)针对执行,我们必须尽量去验证每一次程序执行的合法性。
(4)针对提权,我们应确保补上了所有已知的提权漏洞,并监控各种可疑的提权行为。
(5)针对持久驻留,我们应对系统中的“自启动”项目、可能获得执行的各种途径做全面的扫描和监控。
(6)针对潜藏,我们需要扫描内核中所有可能导致隐藏、隐匿的钩子、过滤层等,可能得设法发现潜藏的rootkit,或者至少发现异常迹象。
(7)针对命令与控制,我们需要在网络上过滤可疑的流量,还需要监控机器可能的异常行为(如非工作时间的异常操作等)。
(8)针对内网侦察和横向移动,我们必须加强内网每台机器之间的防护,加强每台机器的自身信息(如操作系统的版本)的隐藏与保护,并及时把存在异常迹象的被感染机器隔离出内网。
(9)针对渗出,除了如7中监控可疑流量之外,我们要对内部重要资料进行特别的保护,必要时候需要使用DLP(数据防泄密)手段。当然DLP本来不是用来对付内网渗透攻击的,要起作用需要专门结合主机防御的设计。
(10)针对破坏性的影响,我们除了对关键设施要进行专门防护之外,还需要准备应急响应措施。考虑一下所有内网机器全部染毒报废的情况下,如何能快速清理并恢复?
要注意的是上述措施有些并不适合在主机上实现,也就并不包括在主机防御的范围之中。比如1主要依靠的是规则、审核,而网络流量适合用NIDS来进行监控。
本书涉及的内容主要有关于上述(3)(7)的执行防御、关于(2)的网络防护,关于(5)(6)的全面扫描,关于(8)的内网反侦察,关于(9)的特殊设计的DLP,关于(9)的磁盘还原技术。
1.5防御的优先顺序
当然,在成本有限的情况下,我们应该从最具有性价比的节点开始入手。
如果遍历1.3节中的攻击手段中从(1)-(11)一共11个节点,你会发现在每个节点上投入和收益的性价比是不同的。比如“(10)渗出和“(11)影响”,这两个攻击步骤大概率是发生在计算机被完全控制的基础之上。
在计算机根权限完全被控制的情况下再去阻止或者检测某件事,不用我说你也会明白这究竟有多难!在rootkit[4]的控制下,我们的监控到的系统事件、读取到的文件、甚至调用系统调用返回的结果,统统可能是假的。
难不等于我们不能去做。但是我们要从最具性价比的事开始做起。仔细观察上述节点,你会发现防御和检测的难度有一个陡然升高的过程。原本关键点在上述的“(4)提权”。在这一步之后,恶意代码已经具有根权限。
一般而言安全系统具有系统根权限。不考虑被攻击用户主观恶意的情况下,恶意代码在提权之前的权限低于安全系统的权限。而这一步之后,恶意代码和安全系统的权限已经几乎等同,进入了“公平决斗”的阶段。
上面之所以使用“几乎”,只是因为启动的先后顺序会微妙地影响双方的实力。此时安全系统比恶意代码先启动,依然具有微小的优势,但在“5、持久驻留”之后,恶意代码有可能比安全系统更先启动,优势彻底丧失。
国内的情况又不止如此。因为使用普通用户的权限使用Windows非常不方便(很多操作无法进行),所以大部分人会直接使用管理员权限登录。此时如果这些用户执行不明程序,那么这些程序就直接获得根权限。这一步很难阻止,也并不关键。关键点提前了!
考虑“(3)执行”这一步。在这一步之前,黑客有可能获取系统最高权限吗?如果把受攻击的用户主动泄漏管理员密码此类情况排除在外的话,答案是不可能的。恶意代码必须首先要执行,然后才有后面的可能。
那么更早的节点呢?无论是“(1)网络侦察”还是“(2)初始访问”,都是很难完全我们依赖技术手段阻止的。因为我们不可能写代码禁止黑客使用搜索引擎,也很难写代码完全禁止同事看邮件或浏览网页。这些可以做一些事,但是更多地必须结合管理。
因此1.1节中的山羊胡子其实注意到了一个很重要的细节:那就是“(3)执行”是一个非常关键的节点。这个节点上我们可以用技术的手段,去监控所有在操作系统上执行起来的“东西”,在这个节点上进行开发的“性价比”相当高。
在执行之后,网络防护是另一种性价比稍高的手段。虽然我们无法“防止”用户访问外网,但我们可疑对用户对外网进行的系列访问做一番过滤。比如只允许用户访问某些白名单网站,就比允许用户访问任何网站要安全了很多。
在此之后,如果主机已经被病毒感染,我们将处于非常不利的局势,但此时也绝不能彻底躺平。尽可能地进行扫描,发现潜藏的病毒和其他恶意代码总是有好处的。虽然系统不一定能干净地清除病毒,但至少可以及时将被感染的机器隔离出网络。
如果连机器被感染了我们都无从得知,那么至少没有被感染的机器自身要具备一定的反内网侦察、防止被发现漏洞迅速感染的能力。
最坏的情况下,内网机器完全被不明人士所控制,我们能做的也就是保护最重要的机密资料不会外泄,系统不会被彻底摧毁了。
如果系统已经被彻底摧毁,那么我们最后压箱底的手段自然是应急响应,尽快摆脱困境并恢复正常的工作。
以上也是将本书讲述的顺序。本书将分为模块执行防御篇、执行流防御篇、脚本防御篇、内网限制篇、渗出防御篇。
我们将从最为关键的节点开始,层层设防,节节抵抗,用技术手段打造我们的基于主机的防御体系。
[1]Virtual Private Network,虚拟专用网络。
[2]本书中根权限是指操作系统中的最高权限。
[3]MITRE ATT&CK是一个针对网络攻击行为的知识库和模型,详见MITRE官方文档。
[4]本书中rootkit指具有系统根权限的恶意代码。
二
第2章模块防御的设计思想
2.1 执行与模块执行
本章内容为介绍模块执行防御。在此我将先介绍“执行”分类,以及“模块执行”在“执行”中的位置和重要性。
2.1.1 初次执行
恶意代码(或者行为)要在被攻击的机器上执行起来,看起来似乎一定要让被攻击的处理器执行一些指令。但这种想法并不正确。
在《渗透测试高手:打造固若金汤的安全网络》(有意思的是这本书的英文书名意义和中文翻译版完全相反,意义为破解最坚固的网络)一书中,作者Wil Allsopp举出了一些例子,比如一个如同转接头一样的东西。他趁别人去吃午饭的时候爬到别人办公桌下面,把键盘线拔了,插上他的“转接头”,然后再把原本直接插在电脑上的键盘线插在转接头上。
这样对方所有的按键都被这个“转接头”记录。而且转接头内置手机卡和具有手机功能的芯片,能源源不断将记录的按键通过移动网络发出,信息随之泄漏。被攻击主机上没有执行恶意任何代码,而且这种硬件很长时间也不会有人发现。
我举这个例子仅为说明情况并不总是如我们想象的那么简单。在我们构筑安全的防线的时候,千万不要有“我这种技术能百分之百防住所有攻击”的想法。在对可能的攻击形式进行分类的时候要尤为小心。就像我打算将恶意代码的执行方式进行分类,也一定会有我未能想到的地方。
现在假定排除所有的恶意硬件以及不需要执行任何恶意代码的社会工程学攻击,仅仅考虑有恶意代码在主机上运行的形式。那么无论如何,恶意代码从并未执行到开始一系列的操作,一定存在一次“初始执行”的过程。所谓的“初始执行”是指恶意代码在被攻击机器上的第一次执行。
恶意代码的初始执行有很多种可能。在我们的现实生活中可以分成如下几种途径:
(1)用户操作直接运行了来自攻击者的恶意的、或者被恶意代码感染的(后续统一称为恶意的)可执行模块。
(2)用户操作同(1),但用户点击的是某种可解释执行的命令或脚本,如.bat,或者Linux的shell脚本,被浏览器解释执行的JavaScript等。
(3)用户操作打开了来自攻击者的某种非可执行模块的东西(如网页、pdf、doc文档、图片等),导致了主机上某个软件(如浏览器)启动,然后发生了恶意代码的执行或者是合法的程序被利用执行了恶意的操作。
(4)用户什么也没有做。但是主机受到来自网络的攻击,系统或某软件自身漏洞或设计缺陷或策略上的漏洞导致了同(3)的后果。
2.1.2 原生执行与解释执行
不管是哪种初始途径,我们总是可以把所有的恶意代码的执行归结为两种:
(1)原生执行。
(2)解释执行。
所谓原生执行是指被攻击的电脑的处理器支持的指令直接在被攻击的处理器上运行。Biru现在的内网电脑绝大部分使用x64的处理器,那么我们可以认为恶意代码本身就是x64指令的情况就称为恶意代码的原生执行。原生执行必须拥有如下两个要素之一:
(1)这些原生指令不属于任何合法的软件,是出于任何合法的软件设计之外的目的编写的。用户直接下载一个恶意的可执行模块就是这种情况。
(2)这些原生指令虽然属于合法的软件,但其执行过程不符合该软件的任何设计。比如一个程序因为缓冲溢出而返回到了错误的地址,然后执行了一串本不该被执行的指令。
如果某服务程序收到了来自远程的命令然后执行了恶意的操作(比如RPC的方式),我并不认为这是一种原生执行。因为该操作虽然恶意,但是服务程序本身就具备提供该操作的功能,因此是符合设计的。
符合设计并不等于没有恶意。此类恶意执行我将归于上述的(2)解释执行。因为它实际上是解释执行了恶意的命令。解释执行将会放在后面讨论,在这里我们只讨论符合上面提到的二个要素之一的原生执行。
原生执行本身的形式是多种多样的。其中最为“规范”的方式是模块执行。
所谓模块是操作系统所规定的一个可执行单元。它保存在硬盘上的时候表现为一个可执行文件(如Windows上的.exe、.dll或者.sys文件,Linux上的.so文件),加载进入内存之后则成为一个可执行映像。
模块并不一定要经过文件-加载-映像-执行的过程。不生成任何文件,直接把模块写入内存生成映像是完全可以做到的。很多恶意代码用这个方式避免扫描硬盘文件方式的检测。
除了模块执行之外,壳代码(shellcode)执行也是原生指令执行方式的一种。
这个地方很容易混淆。所谓壳代码中的“壳”(shell)本意是指Linux中的控制台。因为最早隐藏的这些恶意代码目的是为了能偷偷连接一个控制台来控制这台机器。通过远程控制台来恶意地控制一台机器,这在本书的分类中属于解释执行而非原生执行。因为恶意的是命令而不是原生指令。
但现在这个词语的意义已经发生了很大的转变。目前国内安全行业中所谓“壳代码”特指并非以规范的完整模块形式存在,而是嵌入在模块中、或者是游离于所有模块外的代码。壳代码并非一定是恶意的,但有很多恶意代码是壳代码的形式。
“加壳”也成为了国内安全行业的一个特有名词。所谓的“加壳”的意思是“在原本干净的模块中加入壳代码”。这个操作并不一定是恶意的。很多对模块进行安全加固的操作,就是以壳代码的形式加入安全组件,使得模块更难被破解。
无论是单纯的模块,还是不属于任何模块的壳代码,它们都需要生成新的指令。但实际上还存在一种不需要生成任何指令就能原生执行恶意代码的形式,那就是不写入任何恶意指令,但利用主机上原有指令进行执行。
内存中已经存在的合法模块中已经有很多的指令。如果用一种巧妙的方法,在不改写这些指令的情况下构造一个新的指令路径来执行某些指令,就能实现想要的完成的恶意操作。有兴趣的读者可以自己了解一下ROP执行。
此外在利用漏洞进行壳代码执行之前,也常会利用已有模块中的指令作为跳板来跳转到壳代码。本书中我可以把此类执行统称为“利用执行”,意思是利用原有合法执行实现新的操作的原生执行。
因此,原生执行可以分为模块执行、壳代码执行、利用执行这三种。如果读者认为还有更多的形式,欢迎来信补充。
解释执行是和原生执行相对的,并非直接执行处理器所执行的原生指令,而是由合法的服务器或解释器(比如RPC服务、Python.exe、Windows PowerShell、浏览器、Office、Java虚拟机)去执行命令、解释脚本或者中间码来实现执行的。其执行过程本身是符合设计的,但执行的命令或脚本中含有恶意的内容。这一部分将会在本书后面的章节中再详细分析。本章不会涉及。
总而言之,本书对恶意代码的执行进行了如下的分类:
n 原生执行
u 模块执行
u 壳代码执行
u 利用执行
n 解释执行
本书的第一部分将重点解决模块执行的防御问题。
2.1.3 模块执行
在各种执行方式的分类中,如果从攻击者的角度出发,我们会发现,壳代码执行和利用执行的能力虽然巧妙而且隐蔽,但在使用上是极为不便和受限的。而可执行模块和脚本则几乎具有“无限”的能力。
比如壳代码,它没有模块结构,因此也不存在导入表,如果需要调用系统中其他模块的函数则相当麻烦。这种麻烦并非不可以克服,但很少有人那么去做,因为没有必要。既然壳代码已经执行起来,那从下载一个可执行模块或者恶意脚本来运行很难吗?
我见过很多恶意程序,但完全由壳代码构成、无任何模块组成和脚本成分的恶意程序少之又少。一般攻击者都会将壳代码执行和利用执行当作下载并执行真正的恶意模块或者恶意脚本的跳板。
总而言之,模块执行非常重要。监控住所有的模块执行,或者拦截住非法模块的执行,对内网安全有巨大的意义,是基于主机的入侵检测和防御系统的重要组成部分。
在我个人看来,成功的模块执行防御(完美的成功是不存在的)的技术难度最小,但足以在内网安全中顶小半边天,同时另外小半边天则是范围极广、难度极大的解释执行的防御。
2.2 模块的公开检验措施
在当前现实的世界中,对模块的执行已经存在诸如签名校验、特征扫描等措施。因为这些措施的共同特点都是公开的,所以我将它们称之为“公开检验”措施。本节将会详述这些公开检验措施的效果和局限性。
2.2.1 Windows的可执行文件格式
在模块被加载进入系统之前,一般的载体为可执行文件,在Windows上为PE文件(Portable Executable,这是一种文件格式规范,意为可移植的可执行文件)。Windows中常见扩展名为exe、dll、sys的文件就是PE文件。
理论上初始执行时如果要在用户态环境下加载一个模块到Windows中,模块必须首先作为磁盘文件的形式而存在。
有些恶意代码在执行之后模块文件会消失,但那只不过是自己删除自己而已,并非从未作为文件存在过。有些恶意代码可以直接把模块写入内存而绕过文件加载,但既然恶意代码已经能执行,就不再是初始执行了。
有些带有漏洞的驱动程序提供了接口,调用这些接口可以绕过文件形式,直接往内核中写入模块映像并执行。但这也需要执行调用这些接口的恶意代码先被执行,并非初始执行。
是否存在不需要通过文件的形式存在,就获得初始执行的可执行模块呢?理论上可以为Windows或者某个应用软件设计一个非常离谱的后门或漏洞来做到这一点。
但这并不关键。若是真存在通过漏洞直接执行,而不通过Windows的文件加载方式的模块,我们可以无视它的模块结构,将它视同壳代码处理。因为它不是通过Windows加载,而是通过壳的跳转方式开始执行的。
本书后面的章节会专门讨论壳代码执行的防御。本章中,我们假定任何模块执行的初始执行,恶意模块都必须首先以文件的形式存在。
PE文件的文件名并无明确规定,因此一般认为“.bmp”一定不是可执行文件而“.exe”一定是可执行文件的想法是不对的。
一个随意指定扩展名的PE文件,使用Windows的“LoadLibrary”之类的接口去加载也可以成功。但是PE文件格式的规范不可违反。比如PE文件开头前两个字节必为‘MZ’,倘若不正确,Windows就会拒绝加载这个文件。
任何时候我们可以把系统中所有存在的文件分为两种:
(1)正确格式的PE文件,以Windows可以成功加载并运行它为标准可执行模块。
(2)非PE文件。Windows无法正常加载运行它(解释方式执行除外)。
这时候我们就可以有一个想法:是否如果任何时候我们都确保主机系统中所有的可执行文件都不是恶意的,那么就可以确保主机系统的安全?
这其实就是本书1.1.1节中山羊胡子同事的想法。在不考虑恶意硬件、纯社工攻击、壳代码执行、利用执行、解释执行等一大堆特殊情况的基础上,这个说法是正确的。那么如何确保这些文件都不是恶意的呢?
只要确保两点就可以“认为”这些文件不是恶意的:
(1)这些文件是被信任的,即文件的可信任性。
(2)这些文件没有被修改过,即文件的完整性。
下面我们将专门探讨用来解决这两个问题的公开检验措施。
2.2.2 可执行模块的签名
在我们日常的、对计算机安全的想法中,最常见的一个错觉,就是认为可执行文件的签名能有效确保可执行文件是可信而且完整的。
如今大部分Windows上的可执行模块都有签名,包括Windows系统自带的可执行模块和安装其他软件而带入的可执行模块。Windows上对任何一个可执行文件用鼠标右键点击,弹出菜单中选择“属性”、“数字签名”则可以看到该文件的数字签名。
Windows上可执行文件的数字签名如图2-1所示。
图2-1 Windows上可执行模块的数字签名
有些模块没有签名,那么熟悉中也就不存在“数字签名”这个标签页。没有签名的可执行模型有这么几种情况:
(1)Windows的一些早期模块遗留到了最新系统中,没有签名。
(2)Windows中一些可执行模块是用cab方式打包安装的。cab包有签名,安装之后存在于系统中的可执行模块没有签名。但如果这些模块被修改,会无法通过校验,导致不能加载。
(3)一些不太正规,或者以不太商业的方式发布的软件,比如开源软件,其中可执行模块没有签名。
没有签名的模块虽然存在,但不是主流。后续可以当作特例处理。下面假定我们碰到的正常的模块都是由数字签名的。数字签名是一种算法,分为以下两步:
(1)首先通过摘要算法对文件原有数据进行一轮计算,得到一个摘要值。之后如果文件内容被修改,那么摘要值也得修改,否则就会出现摘要无法匹配内容的情况。
从图2-1看,这个文件实际有两个签名,分别使用了sha1和sha256两种摘要算法。
当然,我们都会想到,既然摘要算法是公开的,那么攻击者修改了可执行模块之后,再计算一次摘要值填回去不就可以了?因此数字签名还有下面的第二步。
(2)签名机构用私钥对算出的摘要进行加密。这样攻击者虽然可以修改文件内容计算出新的摘要,但是因为没有签名机构的私钥而无法对摘要进行加密。
私钥加密的数据用公钥是可以解密的。校验者只要有签名机构的公钥即可解密摘要,验证可执行文件的内容的可靠性。
为了演示效果,我用二进制编辑工具对WeChat.exe这个文件做无关紧要的修改,如图2-2所示。我将PE文件头部常见的字符串“This program cannot be run in DOS mode”中的一个字母“r”改成了“s”。
实际上,修改之后我尝试运行它,发现运行依然是正常的。系统并没有什么异常提示。说明Windows本身并不在执行时校验用户态可执行模块的签名的有效性(但Windows会严格校验内核可执行模块的签名)。
图2-2二进制编辑工具对WeChat.exe这个文件做无关紧要的修改
这时用同前面的方法打开文件属性页面查看签名,似乎也没有什么异常。但如果双击两个签名的其中之一,我们就会发现Windows判定这个签名是无效的,如图2-3所示。
注意这并非是签名的公司有问题,实际上也不是签名有问题,而是文件的内容被修改了,导致内容和签名不匹配。
虽然Windows并不阻止这个模块的执行,但对这种签名和内容不匹配,几乎可以肯定被病毒感染或者因为某种意外被写坏的文件,我们完全可以使用自己的技术手段来阻止执行。
签名似乎同时解决了文件的可信性和完整性问题。
首先文件经过一个机构签名,比如微软,又或者比如英特尔。即便是一个我不认识的公司的签名,那也是一个公司。如果他签名病毒给我,那是跑得了和尚跑不了庙的。然后签名的有效性确保了文件并没有被修改过。
我们并不需要自己来计算哈希,简单校验可执行模块是否有签名,签名是否有效就可以了?这几步都是有Windows提供的API支持的。
图2-3 Windows判定这个签名是无效的
这时候反直觉的知识就来了:就本书写作的时期而言,绝大部分病毒、木马自带的恶意模块都是有签名的!
设计之初,可执行模块的签名的确被设计来解决可信性和完整性问题。其中的理念之一是:虽然我无法知道这个可执行模块是否是恶意的,但至少知道恶意的事是谁做(谁签)的!
如果一个签名签了恶意的模块,那么让更权威的机构吊销签名不就可以了?签名机构为了确保自己的签名不被吊销,也会认真确保不给恶意模块签名吧?
但实际上,最终各种现实的妥协导致了签名的恶意模块满天飞,而签名机构对此能做的极为有限。为什么会导致这种情况呢?
本书写作时,微软还在不断提高Windows内核驱动的认证门槛。EV签名、WHQL认证等措施均在不断强制实施中。等读者读到此书,微软或许已经有了更多更强硬的措施。但该机制的本质缺陷始终没有解决。
有证书就可以对代码进行签名,但绝非是证书持有人只能对“自己”的代码进行签名。证书的颁发机构也无法判断证书持有者到底是在给自己的代码签名,还是在给他人提供“签名服务”。
因为没有签名的代码可能无法在Windows上运行,又或许会被各种安全软件报为非法软件,所以无论是合法软件还是非法软件都会对签名产生强烈的需求。有需求就有市场,有市场就有价格。网络上提供的代码签名服务报价如图2-4所示。
该图是本书写作时在网络上截取的、提供代码签名服务的报价页面。有着强大的经济利益推动,对恶意代码来说,获得签名只是一个价格问题。相比网络黑产可能获得的庞大利益,一年不到一万元的开销只是小菜一碟。
图2-4:网络上提供的代码签名服务报价
既然签名服务总是可以买到的。那么对恶意软件被成功签名就只能寄希望于两条:
1、签名机构对模块的黑白进行准确的判定,不给恶意代码签名。
2、如果某个机构签名的代码最终被公认是恶意的,那么对该机构进行处罚,永久吊销其证书作为震慑。
但偏偏以上两点都不可能实施,或者实施中会出现巨大的变形而失去意义。
我曾见过签名服务提供者要求客户提供自己的全部源码用来检验其中是否存在恶意行为。这种情况很罕见,绝大部分用户不会配合,会选择其他的服务商。但即便客户提供了源码,签名机构就真能发现其中的恶意行为?
假定一份十万行的代码,其中九万九千九百行都实现了非常合理、合法的功能。只是其处在做完某个复杂的行为后,“忘记”了清理缓存并关闭某个端口。此时如果外部黑客连接进来,无需认证就能最终实现远程控制。
那么签名机构仅仅通过阅读代码就能发现这一点?如果客户没有提供源码呢?签名机构通过逆向二进制代码就能发现一个复杂模块中存在的安全漏洞?这到底是安全漏洞还是恶意后门?眼前的黑不是黑,你说的白是什么白?
何况签名机构很可能要对大量的模块以及同个模块的大量版本进行签名,根本没有人力物力去对每个模块进行分析检测。从收益成本比上来说,最优的选择是客户提交模块并付费——机构直接签名并开发票。
让签名机构对模块的黑白进行准确的判定,不给恶意代码签名,无论从技术上说还是成本上考虑都是不现实的。
推而广之,不但任何认证机构,包括微软,或是安全厂商,都无法对一份静态的代码的黑白做出可靠的认证。因为后门、缺陷本质上无法区分。没有缺陷的代码是不存在的,所以绝对安全、不可被恶意利用的代码也不存在。
反而所有的认证机制最终都会变形为某种利益交换。Windows上总有些软件开发者会碰到过开发的软件发布之后,被某些病毒扫描软件报毒导致用户无法使用的情况,最后向相关安全厂商缴纳一笔“服务费”来解决。
然后是第二个问题,证书的吊销。吊销证书对持有证书的签名机构来说,确实是一个很大的震慑。因为毫无疑问,申请新的证书又面临流程上的麻烦和金钱上的消耗。但实际上,吊销证书不是一件简单的事。
我见过大量已泄漏而被吊销的证书,但签名恶意代码之后生成的Windows驱动程序还可以在目前最新版本的Windows11上加载。已吊销签名的证书用来签名恶意代码如图2-5所示。某某科技有限公司的签名私钥泄漏后,被广泛用于黑产签名。
该证书已经被吊销,但签名的驱动竟然还可以加载。是不是又是一件很反常识的事?
图2-5:已吊销签名的证书用来签名恶意代码
微软不能直接禁止一个被吊销的证书签名的恶意模块加载到内核中,这是因为该证书以前签名过很多合法的模块。一旦全部禁止,许多合法的软件将无法正常工作。所谓投鼠忌器就是这个意思。
那微软能不能允许这个证书以前签名的模块加载,但禁止这个证书签名任何新的模块呢?事实上Windows也是这么做的。如果我在今天用这个证书签名一个驱动,那么这个驱动将会被阻止无法加载。
但微软是根据签名时间戳来判断签名时间的。如果签名时间在某个遥远的过去,那么Windows依然认为这个模块是合法的,可以加载。反常识的问题又来了:系统的时间可以设置。只要临时设置系统时间为1999年,完成签名,产生的模块就又可以加载了!
目前流行的黑产签名工具中有些就集成了自动设置系统时间的功能。还有一些是签名后没有时间戳。这种签名的程序将被Windows合法加载。但Windows的内置杀软已经开始拦截无时间戳的黑签名驱动。
此外,想要让签名的时间变得不可伪造,唯一的办法是让微软而不是软件厂商来签名。微软的EV签名强制要求就是将文件发送到微软进行签名,因此EV签名的时间不可伪造。
但即便吊销签名的问题能得到彻底解决,我也不认为证书签名恶意模块的问题能够得到解决。事实上恶意模块并不在意证书是否被吊销。这家被吊销了,再换另一家就行了。
总结这些原因对我们设计未来方案有很大的借鉴意义。
首先签名应该继续存在。它确实杜绝了用感染其他模块方式来隐藏自己的病毒。说明它的意义是存在的。其次恶意模块的签名已经转变为付费行为,门槛提高,完全无成本开发恶意代码的时代开始落幕。
其次,证书的吊销机制有很大的改进空间。现在微软早期签名时间控制机制因为很容易伪造实际失去了意义,EV签名将会一定程度解决这个问题。
但无论如何设计,我们都不能指望签名机制能杜绝恶意可执行模块。它只能提升制作恶意可执行模块的门槛。
2.2.3 恶意代码的特征扫描
现在市面上有许多安全软件,一般都有带有“病毒扫描引擎”。病毒扫描引擎的工作原理是在已知的恶意代码(这里包括了病毒、木马、蠕虫等各类)中截取部分特征码,在系统中的文件或内存中进行检索。如果发现这些特征存在则报告、拦截或清除它们。
这是对文件可信性的另一种认证,和2.2.2节中所述的签名的作用刚好形成互补的关系。签名的作用是认证一个模块是白的(非恶意的、合法的软件)。而特征扫描确认的则是这个模块是黑的。
在这里读者不必深究特征扫描的原理。读者需要明白的是,安装杀毒软件的确能有效地防止恶意模块的加载,但绝没有我们期望的那么完美。我可以举一个简单的例子来说明。
有一个著名的网站名为VirusTotal。请使用搜索引擎搜索VirusTotal并进入官网。该网站提供简单的页面,让用户可以上传任何一个可执行模块的文件。它会使用行业中几乎所有的病毒扫描引擎对该文件进行扫描。任何个人用户都可以免费使用这个网站。
图2-6:一个可执行模块在VirusTotal的扫描结果
一个可执行模块在VirusTotal的扫描结果图2-6所示。我们可以看到,一共有71家病毒扫描引擎对它进行了扫描,只有4家认为是恶意代码。反过来说,有67家病毒扫描引擎认为它并不是恶意代码。
事实上该程序是彻头彻尾的恶意代码。如果只是加载它,它几乎不做任何事情。但它提供了功能强大的接口。调用这些接口,黑产作者可以悄无声息地把任何代码塞进Windows内核隐藏起来,绕过签名、绕过杀软的扫描,实现任何非法目的。
此类产品早已产业化。总地来说,工具厂商提供工具,如具有合法签名、不做任何非法的事,但是具有强大功能的工具。而真正的恶意厂商则可以选择购买或者租用这些工具打包安装到受害者机器上,并顺利将自己的恶意代码注入并隐藏起来,轻松绕过签名和扫描的措施。
图2-7网上各种公开售卖出租各种“合法”程序的广告
本书写作时,网上已存在各种公开售卖或出租的“合法”驱动程序,为“租客”提供驱动隐藏、(任意进程的)内存读写、(任意进程的)隐藏式DLL注入等功能。网上各种公开售卖出租各种“合法”程序的广告如图2-7所示。
它以这种方式提供公开的服务售卖,并在网上声明“严禁用户购买用于非法目的,若有,本站不承担任何责任”,就和毒贩声称“严禁购买用于吸食,一切后果与本贩无关”如出一辙。更讽刺的是该模块还获得了微软的签名。
我在现实对抗中领教过它的威力,我知道这些。而病毒扫描引擎厂商们和微软并不知道。
在这里你也会发现,VirusTotal这个网站其实是黑产代码开发的好助手。如果有人编写了一个恶意的模块,他一定会第一时间把“作品”拖到VirusTotal中进行一轮又一轮的扫描。如果被扫出恶意特征,没有关系,他可以立刻修改代码、加入保护等等不断进行调整。直到所有的扫毒引擎都提示没有恶意特征了,他就可以将恶意代码对外发布了。因此对外发布的恶意模块一定是扫毒引擎扫不到的。
即便是发布出去之后,恶意模块的作者依然可以定时用VirusTotal进行扫描。一旦发现有个别的扫毒引擎开始对该模块报毒,他就可以立刻调整代码,更新版本(大部分恶意软件都具有远程更新能力),将问题扼杀于萌芽之中。
图2-6所示的模块仅仅出现4家引擎报毒,大概率是经过类似方式处理的。之所以还剩下4家引擎报毒,是因为该模块仅在国内提供服务,并不在国外泛滥。剩余4家国外引擎报毒并不影响其“销量”,也就无需继续处理了。
扫毒引擎并不是完全没有用,但它的作用是很有限的。对专业的、被强大经济利益所推动的、有针对性的攻击来说,它的作用更是趋近于零。
2.3 模块的执行防御方案设计
外部通用的如签名、病毒扫描等机制虽然并不完全可靠,对个人用户而言已可以阻挡大部分低成本的一般攻击。但对办公环境而言,一套由内部自行管理的、可执行模块的可信性与完整性认证机制是有必要的。
很多公司的开发办公室里充斥的源代码、原画、3D设计模型等在黑市上都是价值不菲的商品。有时甚至无需黑客动手,内部人士就会主动窃取售卖。我在现实中也接触过中知名游戏公司的办公网络被黑客在长期远程控制多年,持续不断窃取源代码并提供给私服商的案例。
我并不了解所有的行业。但我可以肯定,不同行业会面临不同性质和不同程度的办公室信息安全问题。办公环境面临的是特别针对性的攻击。常见的如1.2.2小节和1.23小节所述的签名、扫毒等措施必然会被无情绕过。任何侥幸心理只会最终让公司蒙受重大损失。
本书不推荐任何商业软件、产品或服务。需要的读者可以自行评估购置。本书仅用示例代码的方式介绍如何进行内网安全软件的开发。请尤其注意的是,本书的方案设计以及展示的代码仅用于教学,未经过详尽的测试和外网实际对抗的考验,仅供学习参考,不可直接商用。
2.3.1 模块执行防御的功能设计
2.2.1节中的有一个假定。我们将这个假定记录并描述如下。
假定2.1:任何模块执行的初始执行,恶意模块都必须首先以文件的形式存在。
注意这个假定之所以能够成立,是建立在我们将所有非文件形式存在的“模块”都视同壳代码处理的基础上的。壳代码执行的防御将是本书中的另一个主题。而本章只讨论以文件形式存在并被Windows以正常方式加载的模块的防御。
基于假定2.1,我们能够设计一个简单有效的模块执行防御系统。其作用是阻止或审计一切以文件形式存在的可疑的可执行模块的执行。
在1.1.1节的对话中,山羊胡子同事实际上已经做出了一个设计。他的设计是在每个可执行模块执行之前,计算该模块文件的散列值,并与已有的文件哈希值白名单库进行比对。如果该散列值在白名单中,则可以正常执行。反之,则拒绝执行。
该方案对于工作环境单一且较少变更(比如工作线或者服务窗口上的电脑)是适用的。但对需要访问网络、查阅很多网络资料、使用各种大型工具软件(如游戏美工和程序员),且工具经常要更新的用户来说就比较难了。可能存在问题。
对任何可执行模块的每次加载重复计算散列值,是一件相当耗费性能的工作。一般软件加载的可执行模块都可能超过数百个,大型软件更不用说。假定每个文件耗费0.1秒时间计算散列值,也能将加载时间拖慢数十秒,这将引起用户反感并最终被抛弃。
这其中存在显而易见的优化空间:如果模块A在第一次运行的时候已经经过了计算确认其散列值在白名单库中,我们又能确认在第二次执行之前它并没有被修改,那么第二次执行时为何还要再次计算散列值呢?
为此我提出一个性能更为优化的设计。其主要思想是:在大部分情况下,使用文件系统路径来代替散列值,只有必要时才使用散列值。在一般的执行过中,只要该文件的路径(实际上包括路径和文件名)不可疑,那么就放它继续执行,不用去计算散列值。
该如何判断一个文件的路径是否可疑?这里我设计一个可疑路径库(后文简称可疑库)。如果一个模块执行时,它的路径位于可疑库中,那么它需要计算散列值才能执行。反之,则可以直接执行。
换句话说,只要不在可疑库中,用户的系统中所有软件都可疑直接执行,不受影响。同时,设计如下规则:
规则2.1:在安全系统初次安装之后,系统中的可疑库为空,不存在任何可疑路径。此时用户系统中所有软件均可正常执行。
这主要是考虑到安全系统的部署时用户工作的连续性。在绝大部分情况下,安全系统并不会在目标环境创建之初就开始部署。
安全系统开始部署时,目标环境(假定是一个办公室)往往已经存在并正常运行了多年。系统上已经安装了工作人员“趁手”的大量软件。贸然阻止用户已有环境中任何软件模块的运行,可能导致工作流程中断,连带导致安全系统的部署实际无法推进。
方便的做法是,在安全系统初次安装时,进行恶意软件扫描清除和和操作系统及漏洞修补升级,然后将所有现存模块均认定安全的。这存在一定的风险,但在普通的工作环境下是可以接受的。
对于更标准的流程,所有的新接入设备都应该使用公司IT部门统一配备的干净软件环境。这种情况下,依然不存在任何可疑模块,所以上述策略同样兼容。
那么何时应产生可疑文件路径呢?设想一下,如果用户自觉或者不自觉从网上下载了一个exe文件,然后双击执行;又或者某个dll文件被下载了并覆盖了系统中一个原有的dll。此时问题产生了:该exe或dll文件是可疑的,不应被简单执行,因此设计如下两条规则。
规则2.2:新的可执行文件创建时,将其路径加入可疑库中。
规则2.3:原有的可执行文件的内容被覆盖或被修改时,若修改后的内容是一个可执行文件,将其路径加入可疑库中。
以上两条就是整个设计的基础。其中有一些隐含的细节。比如一个新的可执行文件的产生不一定是从无到有创建一个新的文件。也有可能是一个原本就存在但并不是可执行文件的文件经过改写之后变成了可执行文件。
一个原有的可执行文件被修改也存在两种情况:第一种是经过修改之后,它依然是可执行文件。这种情况是需要添加可疑路径的。另一种情况是经过修改之后,它不再是可执行文件,这就无需关注了。
但被修改的也可能是可疑路径中的可执行文件(后面简称可疑文件)。可疑文件被修改之后不再是可执行文件,则应该从可疑库中删除。反之则保留。
除了文件内容被修改之外,还有一种情况可能会影响我们的可疑库,那就是文件的改名(Rename)。
要注意的是文件系统概念中的文件改名本质上与文件的移动(Move)等同。无论是路径的修改还是文件名字的修改,都认为是改名。同理,把一个文件从卷[1]一个位置移动到另一个位置也认为是改名。文件的改名无法跨卷。从C盘“移动”一个文件到“D”盘是不可能的,会自动转换成从C盘复制到D盘,然后删除C盘中原文件的操作。
规则2.4:当某可疑文件被改名,那么对应可疑库中的可疑路径应该随之更新。但非可疑文件被改名并不会有任何影响。
完善了关于可疑库的细节,这个设计就完成了大半。接下来是,如果系统中要加载并执行的文件的路径刚好命中了可疑库中的某个路径,那么应该如何处理?
规则2.5:当某可疑文件被执行时,对文件内容计算散列值,如果散列值在散列值白库中,则执行并从可疑库中删除,否则禁止执行并将该散列值上报到后台。有必要的话,同时上报完整样本。
所以除了可疑文件路径库之外,我们还需要一个散列值白库(后面简称白库)。白库中保存着经过公司自身认证过的所有合法、安全、必要的可执行库的散列值。
散列值位于白库中,则这个文件可以直接执行,该路径也应立刻从可疑路径库中删除。这样下次再执行这个文件的时候就不会遇到又要重复计算一次散列值的损耗了。因为所有的可疑文件执行仅计算一次散列值,性能将大幅度提升。
那么如果将来这个文件被又被修改了之后怎么办呢?这种情况下该文件落入规则2.3的管辖范围,被重新加入到可疑列表中。
白库是如何产生的?白库应从该公司认为合法、安全、必要的软件集合中所有的可执行模块库中产生。注意,合法、安全是相对模糊的概念,尤其安全的认证有一定的难度。对公司或组织内部的安全认定来所,最重要的是“必要”。
绝对安全的软件是不存在的,我们需要做的是减少攻击面。如果一个组织体系内固定统一使用某个品牌的浏览器、办公套件和开发工具,那么这些软件就是“必要”的。而除此之外的软件皆为“不必要的”。
我们无需去确认不必要的软件是否安全,只需要一概拒绝即可。这样我们可以把对安全的关注集中在公司必要的少数几款软件上,从而大大提升内网环境的安全性。
这一点非常重要,但很多内网环境对此没有较为严格的认证。有时我们会吐槽柜台办公人员在闲暇时玩扑克游戏,或者管理人员在办公室打开了炒股软件(注意本章仅限模块执行的情况,使用浏览器浏览网页属于解释或者说脚本执行的范畴,其相关问题在本书后文解决),就是缺乏此类限制的典型案例。
但完全严格地执行“非必要则拒绝”又可能导致作茧自缚的后果。比如某部门需要使用某个不常用的小众工具来解决一个独特问题,无法运行可能导致项目无法推进。所以我们需要规则2.5中的上报机制。
当被拒绝执行的模块提交到后台,相关使用人员应有提起申请使用的机制。后台收到这些模块的样本和使用人员提交的申请,应组织管理者和安全人员处理这些申请,确认这些模块是有必要而且安全的,则可加入到白库中。
客户端的机器可以从服务器上下载得到更新的白库,这样原来不可执行的模块现在经过审核之后就可以执行了。
从缩小攻击面的角度出发,白库不应是全公司统一的。如果只有某部门或者某台机器需要某一个特殊的工具,在下发给该部门或者该机器的白库中添加其散列值即可。这样使用该工具的权限仅限于某个范围。
到此为止我们基本完成了模块执行防御的方案设计。注意本方案的设计目标在于监控或阻止恶意模块的初始执行。换句话说,它是在当前系统并未沦陷情况下运行的方案。所以我们暂不探讨它本身被破坏(比如遭遇恶意停止、删除规则等情况)下的安全性。
内网安全方案应该是强制执行,无法被用户手动关闭的(如果用户能手动关闭,那么大概率用户会因为五花八门的理由和借口关闭它)。防止关闭属于方案自保护的范围。本书后面会有专门的章节讨论安全方案的自保护。
2.3.2 模块执行防御的技术选择
在任何环境下开发确保内网安全的软件,应尽量将技术实现做在底层。这是因为所有软件系统中,上层的应用依赖操作系统内核,而操作系统内核依赖硬件的实现。下层的实现总是可以干扰上层获得的结果。而反过来则不行。因此上层与下层的对抗是不平等的。
我们很难预测复杂的系统在哪一层会有漏洞、哪一层可能被恶意代码感染,不断往底层做总是没错的。但同时我们必须考虑技术上的可行性与成本。
比如苹果限制了任何第三方开发者染指IOS的内核(然而非法的黑产作者并不受此限),对此我们只能说一句由他去吧。我不相信任何安全厂商声称的IOS上开发的安全软件,那只是印刷着“保险柜”三个字的瓦楞纸箱。
市面上大部分Android手机也类似。手机厂商限制了安全厂商接触系统内核。手机上几乎没有什么安全系统存在。那是因为应用市场限制带来的虚假和平给了人们很强的安全感,从此再也不需要安全防护软件了。
好在目前手机并不是办公网内最常用的开发工具。否则如果有人专门针对布满了Android或者IOS系统的企业内网开发APT,将如入无人之境。实际上如果一个机构需要使用自己能掌控安全的移动设备,那将不得不进行设备定制,而不是直接购买普通用户使用的产品。
出于对企业内网安全的考虑起见,我对安全系统的客户端部分的开发的技术选择建议如下:
(1)如果我们参与硬件的设计,那么将一些基础的安全功能做在硬件里是最好的选择。
(2)如果我们参与操作系统的设计,那么就应该将一些安全功能实现做在操作系统内核中。
(3)如果我们不能参与操作系统的设计,但是可以利用操作系统的内核提供的接口来实现内核模块,那么就把安全功能做在内核模块里。
(4)如果我们不能触及操作系统内核,但操作系统留有若干底层接口让我们实现部分安全功能,那么就尽量利用这些接口。
(5)如果我们既无法触及操作系统的内核、操作系统也未给安全系统留下足够底层的接口,那么我不推荐将该系统用于作为企业的主要生产力工具,因为其安全性很难保证。
假定有一天我们必须使用某种平板电脑作为主要的设计和开发工具,那么我可以肯定,自定制的Linux平板只要正确地配置了安全特性、加入了基础的内部安全防御系统,会比某些消费级的通用平板产品安全性要好很多。
幸运的是,Windows依然允许安全厂商开发内核驱动模块加载到Windows内核中。因此本书的设计依然可以在Windows上得以实现。
我们在2.3.1节中设计的功能所需的核心技术实现有:
(1)捕获并可拦截任何文件的内容写入,以便以确认该文件经过写入之后是否变成了一个可执行模块,或者可执行文件被修改了而且修改后还是一个可执行文件。
(2)捕获任何文件的改名(本质是移动),以便监控可疑的可执行文件路径的变化,并据此更新可疑库。
(3)捕获并可阻止任何可执行模块被加载执行的事件,以便验证可疑库命中的可执行文件,计算散列值并在需要时上报相关信息。
“捕获事件”是基于主机的入侵防御系统的技术实现中的一个常见要求。我们需要能及时捕获系统中的各种事件,进行安全与否的判断之后再予以放行。如果没有这个能力,即便能通过事后的扫描发现恶意代码的存在,安全事件也已经发生,则只能称为“检测”而不能称之为“防御”了。
以上需求可以使用Windows的文件系统微过滤驱动(Minifilter Driver,后文均称为Minifilter Driver)来方便的实现。微过滤驱动运行在Windows的内核中,已经足够底层,能够满足我们的需求。
在第3章中,我将展示具体的代码实现。
[1]本书中的卷(Volume)是文件系统中的概念,对应磁盘中的一个逻辑分区,是文件系统能处理的最大存储单位。日常操作中可以将存有文件的C盘、D盘等各看成一个卷。
看雪ID:星星人
https://bbs.kanxue.com/user-home-143652.htm
# 往期推荐
3、阿里云CTF2024-暴力ENOTYOURWORLD题解
4、Hypervisor From Scratch - 基本概念和配置测试环境、进入 VMX 操作
球分享
球点赞
球在看
点击阅读原文查看更多